--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 60a4c6ab8b159c465c663848e8e75326d07430ee
Parents : e3116c8
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-07-02T22:19:55-05:00
feat(qrScanner): implement QR code scanning functionality with camera support and error handling in Vue components
Changes
11 files changed, 354 insertions(+), 166 deletions(-)
Diff
diff --git a/.gitignore b/.gitignore
index 4ec723a1..adbefbb9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,6 +28,7 @@ ENV/
env.bak/
venv.bak/
.venv/
+.venv-x64/
# flatpak-builder / snapcraft working trees (large; may contain root-owned cache)
.flatpak-builder/
diff --git a/android/app/src/main/java/com/meshchatx/MainActivity.java b/android/app/src/main/java/com/meshchatx/MainActivity.java
index 15d9f13c..4ea3ac45 100644
--- a/android/app/src/main/java/com/meshchatx/MainActivity.java
+++ b/android/app/src/main/java/com/meshchatx/MainActivity.java
@@ -497,6 +497,7 @@ public class MainActivity extends AppCompatActivity {
private void requestRuntimePermissionsIfNeeded() {
List<String> missingPermissions = new ArrayList<>();
addIfMissing(missingPermissions, Manifest.permission.RECORD_AUDIO);
+ addIfMissing(missingPermissions, Manifest.permission.CAMERA);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
addIfMissing(missingPermissions, Manifest.permission.BLUETOOTH_CONNECT);
addIfMissing(missingPermissions, Manifest.permission.BLUETOOTH_SCAN);
diff --git a/electron/assets/js/tailwindcss/tailwind-v3.4.3-forms-v0.5.7.js b/electron/assets/js/tailwindcss/tailwind-v3.4.3-forms-v0.5.7.js
index 5f8bd4a6..598b9985 100644
--- a/electron/assets/js/tailwindcss/tailwind-v3.4.3-forms-v0.5.7.js
+++ b/electron/assets/js/tailwindcss/tailwind-v3.4.3-forms-v0.5.7.js
@@ -1766,7 +1766,7 @@
for (let n of d0(t)) {
let a = 0,
s = r;
- for (; s != null && a < n.length; )
+ for (; s != null && a < n.length;)
((s = s[n[a++]]), (s = Et(s) && (n.alpha === void 0 || a <= n.length - 1) ? s(e, bs) : s));
if (s !== void 0) {
if (n.alpha !== void 0) {
@@ -2975,7 +2975,7 @@
}
body(e) {
let t = e.nodes.length - 1;
- for (; t > 0 && e.nodes[t].type === "comment"; ) t -= 1;
+ for (; t > 0 && e.nodes[t].type === "comment";) t -= 1;
let i = this.raw(e, "semicolon");
for (let n = 0; n < e.nodes.length; n++) {
let a = e.nodes[n],
@@ -3146,7 +3146,7 @@
: (i = this.raw(e, null, "beforeClose"));
let n = e.parent,
a = 0;
- for (; n && n.type !== "root"; ) ((a += 1), (n = n.parent));
+ for (; n && n.type !== "root";) ((a += 1), (n = n.parent));
if (
i.includes(`
`)
@@ -3286,7 +3286,7 @@
}
root() {
let e = this;
- for (; e.parent && e.parent.type !== "document"; ) e = e.parent;
+ for (; e.parent && e.parent.type !== "document";) e = e.parent;
return e;
}
raw(e, t) {
@@ -3397,7 +3397,7 @@
if (this[Ti]) {
this[Ti] = !1;
let e = this;
- for (; (e = e.parent); ) e[Ti] = !1;
+ for (; (e = e.parent);) e[Ti] = !1;
}
}
get proxyOf() {
@@ -3913,7 +3913,7 @@
s = y;
break;
} else _("bracket");
- for (d = s; i.charCodeAt(d - 1) === Fi; ) ((d -= 1), (f = !f));
+ for (d = s; i.charCodeAt(d - 1) === Fi;) ((d -= 1), (f = !f));
} while (f);
((b = ["brackets", i.slice(y, s + 1), y, s]), (y = s));
} else
@@ -3931,7 +3931,7 @@
s = y + 1;
break;
} else _("string");
- for (d = s; i.charCodeAt(d - 1) === Fi; ) ((d -= 1), (f = !f));
+ for (d = s; i.charCodeAt(d - 1) === Fi;) ((d -= 1), (f = !f));
} while (f);
((b = ["string", i.slice(y, s + 1), y, s]), (y = s));
break;
@@ -3945,7 +3945,7 @@
break;
}
case Fi: {
- for (s = y, c = !0; i.charCodeAt(s + 1) === Fi; ) ((s += 1), (c = !c));
+ for (s = y, c = !0; i.charCodeAt(s + 1) === Fi;) ((s += 1), (c = !c));
if (
((a = i.charCodeAt(s + 1)),
c &&
@@ -3957,7 +3957,7 @@
a !== Ni &&
((s += 1), zf.test(i.charAt(s))))
) {
- for (; zf.test(i.charAt(s + 1)); ) s += 1;
+ for (; zf.test(i.charAt(s + 1));) s += 1;
i.charCodeAt(s + 1) === xr && (s += 1);
}
((b = ["word", i.slice(y, s + 1), y, s]), (y = s));
@@ -4151,7 +4151,7 @@
}
parse() {
let e;
- for (; !this.tokenizer.endOfFile(); )
+ for (; !this.tokenizer.endOfFile();)
switch (((e = this.tokenizer.nextToken()), e[0])) {
case "space":
this.spaces += e[1];
@@ -4200,7 +4200,7 @@
o = e[1].startsWith("--"),
u = [],
c = e;
- for (; c; ) {
+ for (; c;) {
if (((i = c[0]), u.push(c), i === "(" || i === "[")) (a || (a = c), s.push(i === "(" ? ")" : "]"));
else if (o && n && i === "{") (a || (a = c), s.push("}"));
else if (s.length === 0)
@@ -4221,7 +4221,7 @@
}
if ((this.tokenizer.endOfFile() && (t = !0), s.length > 0 && this.unclosedBracket(a), t && n)) {
if (!o)
- for (; u.length && ((c = u[u.length - 1][0]), !(c !== "space" && c !== "comment")); )
+ for (; u.length && ((c = u[u.length - 1][0]), !(c !== "space" && c !== "comment"));)
this.tokenizer.back(u.pop());
this.decl(u, o);
} else this.unknownWord(u);
@@ -4244,14 +4244,14 @@
e[0][0] !== "word";
)
(e.length === 1 && this.unknownWord(e), (i.raws.before += e.shift()[1]));
- for (i.source.start = this.getPosition(e[0][2]), i.prop = ""; e.length; ) {
+ for (i.source.start = this.getPosition(e[0][2]), i.prop = ""; e.length;) {
let c = e[0][0];
if (c === ":" || c === "space" || c === "comment") break;
i.prop += e.shift()[1];
}
i.raws.between = "";
let a;
- for (; e.length; )
+ for (; e.length;)
if (((a = e.shift()), a[0] === ":")) {
i.raws.between += a[1];
break;
@@ -4259,7 +4259,7 @@
(i.prop[0] === "_" || i.prop[0] === "*") && ((i.raws.before += i.prop[0]), (i.prop = i.prop.slice(1)));
let s = [],
o;
- for (; e.length && ((o = e[0][0]), !(o !== "space" && o !== "comment")); ) s.push(e.shift());
+ for (; e.length && ((o = e[0][0]), !(o !== "space" && o !== "comment"));) s.push(e.shift());
this.precheckMissedSemicolon(e);
for (let c = e.length - 1; c >= 0; c--) {
if (((a = e[c]), a[1].toLowerCase() === "!important")) {
@@ -4294,7 +4294,7 @@
o = !1,
u = [],
c = [];
- for (; !this.tokenizer.endOfFile(); ) {
+ for (; !this.tokenizer.endOfFile();) {
if (
((e = this.tokenizer.nextToken()),
(i = e[0]),
@@ -4313,7 +4313,7 @@
break;
} else if (i === "}") {
if (u.length > 0) {
- for (a = u.length - 1, n = u[a]; n && n[0] === "space"; ) n = u[--a];
+ for (a = u.length - 1, n = u[a]; n && n[0] === "space";) n = u[--a];
n && (t.source.end = this.getPosition(n[3] || n[2]));
}
this.end(e);
@@ -4398,20 +4398,19 @@
spacesAndCommentsFromEnd(e) {
let t,
i = "";
- for (; e.length && ((t = e[e.length - 1][0]), !(t !== "space" && t !== "comment")); )
- i = e.pop()[1] + i;
+ for (; e.length && ((t = e[e.length - 1][0]), !(t !== "space" && t !== "comment"));) i = e.pop()[1] + i;
return i;
}
spacesAndCommentsFromStart(e) {
let t,
i = "";
- for (; e.length && ((t = e[0][0]), !(t !== "space" && t !== "comment")); ) i += e.shift()[1];
+ for (; e.length && ((t = e[0][0]), !(t !== "space" && t !== "comment"));) i += e.shift()[1];
return i;
}
spacesFromEnd(e) {
let t,
i = "";
- for (; e.length && ((t = e[e.length - 1][0]), t === "space"); ) i = e.pop()[1] + i;
+ for (; e.length && ((t = e[e.length - 1][0]), t === "space");) i = e.pop()[1] + i;
return i;
}
stringFrom(e, t) {
@@ -4477,13 +4476,13 @@
(t = e) => {
let i = "",
n = t;
- for (; n--; ) i += r[(Math.random() * r.length) | 0];
+ for (; n--;) i += r[(Math.random() * r.length) | 0];
return i;
},
X0 = (r = 21) => {
let e = "",
t = r;
- for (; t--; ) e += Q0[(Math.random() * 64) | 0];
+ for (; t--;) e += Q0[(Math.random() * 64) | 0];
return e;
};
sc.exports = { nanoid: X0, customAlphabet: J0 };
@@ -4546,7 +4545,7 @@
else {
let a = i.length - 2,
s;
- for (; n < a; )
+ for (; n < a;)
if (((s = n + ((a - n) >> 1)), e < i[s])) a = s - 1;
else if (e >= i[s + 1]) n = s + 1;
else {
@@ -4815,7 +4814,7 @@
}
if ((this.prepareVisitors(), this.hasListener)) {
let e = this.result.root;
- for (; !e[qe]; ) ((e[qe] = !0), this.walkSync(e));
+ for (; !e[qe];) ((e[qe] = !0), this.walkSync(e));
if (this.listeners.OnceExit)
if (e.type === "document")
for (let t of e.nodes) this.visitSync(this.listeners.OnceExit, t);
@@ -4906,10 +4905,10 @@
}
if ((this.prepareVisitors(), this.hasListener)) {
let e = this.result.root;
- for (; !e[qe]; ) {
+ for (; !e[qe];) {
e[qe] = !0;
let t = [mc(e)];
- for (; t.length > 0; ) {
+ for (; t.length > 0;) {
let i = this.visitTick(t);
if (_r(i))
try {
@@ -4976,7 +4975,7 @@
if (t.iterator !== 0) {
let s = t.iterator,
o;
- for (; (o = i.nodes[i.indexes[s]]); )
+ for (; (o = i.nodes[i.indexes[s]]);)
if (((i.indexes[s] += 1), !o[qe])) {
((o[qe] = !0), e.push(mc(o)));
return;
@@ -4984,7 +4983,7 @@
((t.iterator = 0), delete i.indexes[s]);
}
let a = t.events;
- for (; t.eventIndex < a.length; ) {
+ for (; t.eventIndex < a.length;) {
let s = a[t.eventIndex];
if (((t.eventIndex += 1), s === It)) {
i.nodes && i.nodes.length && ((i[qe] = !0), (t.iterator = i.getIterator()));
@@ -5370,7 +5369,7 @@ https://www.w3ctech.com/topic/2226`
Zi.default = Wv;
function Wv(r) {
for (var e = arguments.length, t = new Array(e > 1 ? e - 1 : 0), i = 1; i < e; i++) t[i - 1] = arguments[i];
- for (; t.length > 0; ) {
+ for (; t.length > 0;) {
var n = t.shift();
if (!r[n]) return;
r = r[n];
@@ -5386,7 +5385,7 @@ https://www.w3ctech.com/topic/2226`
en.default = Gv;
function Gv(r) {
for (var e = arguments.length, t = new Array(e > 1 ? e - 1 : 0), i = 1; i < e; i++) t[i - 1] = arguments[i];
- for (; t.length > 0; ) {
+ for (; t.length > 0;) {
var n = t.shift();
(r[n] || (r[n] = {}), (r = r[n]));
}
@@ -5399,7 +5398,7 @@ https://www.w3ctech.com/topic/2226`
tn.__esModule = !0;
tn.default = Hv;
function Hv(r) {
- for (var e = "", t = r.indexOf("/*"), i = 0; t >= 0; ) {
+ for (var e = "", t = r.indexOf("/*"), i = 0; t >= 0;) {
e = e + r.slice(i, t);
var n = r.indexOf("*/", t + 2);
if (n < 0) return e;
@@ -5704,7 +5703,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
return this;
}),
(t.removeAll = function () {
- for (var n = gx(this.nodes), a; !(a = n()).done; ) {
+ for (var n = gx(this.nodes), a; !(a = n()).done;) {
var s = a.value;
s.parent = void 0;
}
@@ -7028,7 +7027,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
if (r.safe) ((t += R), (w = t.length - 1));
else throw r.error("Unclosed " + q, s, o - a, o);
}
- for (; o < n; ) {
+ for (; o < n;) {
switch (((c = t.charCodeAt(o)), c === O.newline && ((a = o), (s += 1)), c)) {
case O.space:
case O.tab:
@@ -7198,7 +7197,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
);
}
function Ia(r, e) {
- for (var t = -1, i = []; (t = r.indexOf(e, t + 1)) !== -1; ) i.push(t);
+ for (var t = -1, i = []; (t = r.indexOf(e, t + 1)) !== -1;) i.push(t);
return i;
}
function Vk() {
@@ -7249,7 +7248,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
};
if (a === 1 && !~[T.word].indexOf(i[0][A.FIELDS.TYPE]))
return this.expected("attribute", i[0][A.FIELDS.START_POS]);
- for (var o = 0, u = "", c = "", f = null, d = !1; o < a; ) {
+ for (var o = 0, u = "", c = "", f = null, d = !1; o < a;) {
var p = i[o],
m = this.content(p),
b = i[o + 1];
@@ -7622,7 +7621,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
if ((this.position++, i && i.type === $k.PSEUDO)) {
var a = new _a.default({ source: { start: Ap(this.tokens[this.position - 1]) } }),
s = this.current;
- for (i.append(a), this.current = a; this.position < this.tokens.length && n; )
+ for (i.append(a), this.current = a; this.position < this.tokens.length && n;)
(this.currToken[A.FIELDS.TYPE] === T.openParenthesis && n++,
this.currToken[A.FIELDS.TYPE] === T.closeParenthesis && n--,
n
@@ -7632,7 +7631,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
this.position++));
this.current = s;
} else {
- for (var o = this.currToken, u = "(", c; this.position < this.tokens.length && n; )
+ for (var o = this.currToken, u = "(", c; this.position < this.tokens.length && n;)
(this.currToken[A.FIELDS.TYPE] === T.openParenthesis && n++,
this.currToken[A.FIELDS.TYPE] === T.closeParenthesis && n--,
(c = this.currToken),
@@ -7767,7 +7766,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
return n && this.content(n) === "|" ? (this.position++, this.namespace()) : this.splitWord(i);
}),
(e.loop = function () {
- for (; this.position < this.tokens.length; ) this.parse(!0);
+ for (; this.position < this.tokens.length;) this.parse(!0);
return (this.current._inferEndPosition(), this.root);
}),
(e.parse = function (i) {
@@ -7862,7 +7861,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
}),
(e.locateNextMeaningfulToken = function (i) {
i === void 0 && (i = this.position + 1);
- for (var n = i; n < this.tokens.length; )
+ for (var n = i; n < this.tokens.length;)
if (jk[this.tokens[n][A.FIELDS.TYPE]]) {
n++;
continue;
@@ -8295,7 +8294,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
}
function cn(r, e) {
let t = r.prev();
- for (e.after(r); t && t.type === "comment"; ) {
+ for (e.after(r); t && t.type === "comment";) {
let i = t.prev();
(e.after(t), (t = i));
}
@@ -8357,7 +8356,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
function NS(r) {
let e = [],
t = r.parent;
- for (; t && t instanceof BS; ) (e.push(t), (t = t.parent));
+ for (; t && t instanceof BS;) (e.push(t), (t = t.parent));
return e;
}
function $S(r) {
@@ -8388,7 +8387,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
((c = f.clone({ nodes: [] })), m && c.append(m));
let b = [],
y = (p[d - 1] || r).next();
- for (; y; ) (b.push(y), (y = y.next()));
+ for (; y;) (b.push(y), (y = y.next()));
c.append(b);
}),
c && (s || t[t.length - 1]).after(c));
@@ -11255,8 +11254,8 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
}
function Id(r) {
let e = [];
- for (; r.prev() && r.prev().type !== "combinator"; ) r = r.prev();
- for (; r && r.type !== "combinator"; ) (e.push(r), (r = r.next()));
+ for (; r.prev() && r.prev().type !== "combinator";) r = r.prev();
+ for (; r && r.type !== "combinator";) (e.push(r), (r = r.next()));
return e;
}
function w2(r) {
@@ -11334,7 +11333,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
if (!a) return;
let s = [],
o = i.next();
- for (; o && o.type !== "combinator"; ) (s.push(o), (o = o.next()));
+ for (; o && o.type !== "combinator";) (s.push(o), (o = o.next()));
let u = o;
(a.pseudo.parent.insertAfter(a.pseudo, Le.default.selector({ nodes: s.map((c) => c.clone()) })),
i.remove(),
@@ -11380,7 +11379,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
}
function* x2(r) {
let e = 1 / 0;
- for (; e >= 0; ) {
+ for (; e >= 0;) {
let t,
i = !1;
if (e === 1 / 0 && r.endsWith("]")) {
@@ -13336,7 +13335,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
return typeof t == "function" ? t(e) : t + e;
}
function* Fh(r) {
- for (yield r; r.parent; ) (yield r.parent, (r = r.parent));
+ for (yield r; r.parent;) (yield r.parent, (r = r.parent));
}
function gC(r, e = {}) {
let t = r.nodes;
@@ -13689,7 +13688,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
((i = d), (n = p === Ln ? "'" : '"'), (s = { type: "string", sourceIndex: d, quote: n }));
do
if (((o = !1), (i = t.indexOf(n, i + 1)), ~i))
- for (u = i; t.charCodeAt(u - 1) === Mo; ) ((u -= 1), (o = !o));
+ for (u = i; t.charCodeAt(u - 1) === Mo;) ((u -= 1), (o = !o));
else ((t += n), (i = t.length - 1), (s.unclosed = !0));
while (o);
((s.value = t.slice(d + 1, i)),
@@ -13736,7 +13735,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
i -= 1;
do
if (((o = !1), (i = t.indexOf(")", i + 1)), ~i))
- for (u = i; t.charCodeAt(u - 1) === Mo; ) ((u -= 1), (o = !o));
+ for (u = i; t.charCodeAt(u - 1) === Mo;) ((u -= 1), (o = !o));
else ((t += ")"), (i = t.length - 1), (s.unclosed = !0));
while (o);
c = i;
@@ -13894,14 +13893,14 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
)
e += 1;
if (((i = r.charCodeAt(e)), (n = r.charCodeAt(e + 1)), i === Lo && n >= 48 && n <= 57))
- for (e += 2; e < t && ((i = r.charCodeAt(e)), !(i < 48 || i > 57)); ) e += 1;
+ for (e += 2; e < t && ((i = r.charCodeAt(e)), !(i < 48 || i > 57));) e += 1;
if (
((i = r.charCodeAt(e)),
(n = r.charCodeAt(e + 1)),
(a = r.charCodeAt(e + 2)),
(i === _C || i === OC) && ((n >= 48 && n <= 57) || ((n === zn || n === $n) && a >= 48 && a <= 57)))
)
- for (e += n === zn || n === $n ? 3 : 2; e < t && ((i = r.charCodeAt(e)), !(i < 48 || i > 57)); ) e += 1;
+ for (e += n === zn || n === $n ? 3 : 2; e < t && ((i = r.charCodeAt(e)), !(i < 48 || i > 57));) e += 1;
return { number: r.slice(0, e), unit: r.slice(e) };
};
});
@@ -13969,16 +13968,14 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
}
return { isValid: !1, error: u };
}
- if (
- !(
- typeof s == "string" ||
- typeof s == "number" ||
- typeof s == "function" ||
- s instanceof String ||
- s instanceof Number ||
- Array.isArray(s)
- )
- ) {
+ if (!(
+ typeof s == "string" ||
+ typeof s == "number" ||
+ typeof s == "function" ||
+ s instanceof String ||
+ s instanceof Number ||
+ Array.isArray(s)
+ )) {
let u = `'${n}' was found but does not resolve to a string.`;
if ($o(s)) {
let c = Object.keys(s).filter((f) => zo(r, [...a, f]).isValid);
@@ -15345,7 +15342,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
((i = d), (n = p === Yn ? "'" : '"'), (s = { type: "string", sourceIndex: d, quote: n }));
do
if (((o = !1), (i = t.indexOf(n, i + 1)), ~i))
- for (u = i; t.charCodeAt(u - 1) === el; ) ((u -= 1), (o = !o));
+ for (u = i; t.charCodeAt(u - 1) === el;) ((u -= 1), (o = !o));
else ((t += n), (i = t.length - 1), (s.unclosed = !0));
while (o);
((s.value = t.slice(d + 1, i)),
@@ -15392,7 +15389,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
i -= 1;
do
if (((o = !1), (i = t.indexOf(")", i + 1)), ~i))
- for (u = i; t.charCodeAt(u - 1) === el; ) ((u -= 1), (o = !o));
+ for (u = i; t.charCodeAt(u - 1) === el;) ((u -= 1), (o = !o));
else ((t += ")"), (i = t.length - 1), (s.unclosed = !0));
while (o);
c = i;
@@ -15550,14 +15547,14 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
)
e += 1;
if (((i = r.charCodeAt(e)), (n = r.charCodeAt(e + 1)), i === il && n >= 48 && n <= 57))
- for (e += 2; e < t && ((i = r.charCodeAt(e)), !(i < 48 || i > 57)); ) e += 1;
+ for (e += 2; e < t && ((i = r.charCodeAt(e)), !(i < 48 || i > 57));) e += 1;
if (
((i = r.charCodeAt(e)),
(n = r.charCodeAt(e + 1)),
(a = r.charCodeAt(e + 2)),
(i === sA || i === aA) && ((n >= 48 && n <= 57) || ((n === Xn || n === Jn) && a >= 48 && a <= 57)))
)
- for (e += n === Xn || n === Jn ? 3 : 2; e < t && ((i = r.charCodeAt(e)), !(i < 48 || i > 57)); ) e += 1;
+ for (e += n === Xn || n === Jn ? 3 : 2; e < t && ((i = r.charCodeAt(e)), !(i < 48 || i > 57));) e += 1;
return { number: r.slice(0, e), unit: r.slice(e) };
};
});
@@ -16396,7 +16393,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
}
remove(e, t) {
let i = 0;
- for (; i < e.length; ) {
+ for (; i < e.length;) {
if (!this.isNot(e[i - 1]) && this.isProp(e[i]) && this.isOr(e[i + 1])) {
if (this.toRemove(e[i][0], t)) {
e.splice(i, 2);
@@ -16480,7 +16477,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
isHack(e) {
let t = e.parent.index(e) + 1,
i = e.parent.nodes;
- for (; t < i.length; ) {
+ for (; t < i.length;) {
let n = i[t].selector;
if (!n) return !0;
if (n.includes(this.unprefixed) && n.match(this.nameRegexp)) return !1;
@@ -16542,7 +16539,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
}
already(e, t, i) {
let n = e.parent.index(e) - 1;
- for (; n >= 0; ) {
+ for (; n >= 0;) {
let a = e.parent.nodes[n];
if (a.type !== "rule") return !1;
let s = !1;
@@ -17348,7 +17345,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
ir = class extends A5 {
keyframeParents(e) {
let { parent: t } = e;
- for (; t; ) {
+ for (; t;) {
if (t.type === "atrule" && t.name === "keyframes") return !0;
({ parent: t } = t);
}
@@ -18424,7 +18421,7 @@ In order to be iterable, non-array objects must have a [Symbol.iterator]() metho
{ length: n } = t.nodes,
a = this.unprefixed(e.prop),
s = (o, u) => {
- for (i += o; i >= 0 && i < n; ) {
+ for (i += o; i >= 0 && i < n;) {
let c = t.nodes[i];
if (c.type === "decl") {
if (
diff --git a/meshchatx/src/frontend/components/contacts/ContactsPage.vue b/meshchatx/src/frontend/components/contacts/ContactsPage.vue
index 3bd9c793..d7711f0f 100644
--- a/meshchatx/src/frontend/components/contacts/ContactsPage.vue
+++ b/meshchatx/src/frontend/components/contacts/ContactsPage.vue
@@ -433,6 +433,13 @@ import WebSocketConnection from "../../js/WebSocketConnection";
import ToastUtils from "../../js/ToastUtils";
import DownloadUtils from "../../js/DownloadUtils";
import DialogUtils from "../../js/DialogUtils";
+import {
+ attachStreamToVideo,
+ decodeQrFromVideo,
+ describeCameraError as describeQrCameraError,
+ isCameraSupported,
+ startCameraStream,
+} from "../../js/qrScannerUtils";
import LxmfUserIcon from "../LxmfUserIcon.vue";
import ContextMenuDivider from "../contextmenu/ContextMenuDivider.vue";
@@ -488,11 +495,7 @@ export default {
},
computed: {
cameraSupported() {
- return (
- typeof window !== "undefined" &&
- typeof window.BarcodeDetector !== "undefined" &&
- navigator?.mediaDevices?.getUserMedia
- );
+ return isCameraSupported();
},
hasMoreContacts() {
return this.contacts.length < this.totalContactsCount;
@@ -900,10 +903,7 @@ export default {
return;
}
try {
- const stream = await navigator.mediaDevices.getUserMedia({
- video: { facingMode: "environment" },
- audio: false,
- });
+ const stream = await startCameraStream();
if (!stream.getVideoTracks().length) {
this.scannerError = this.$t("contacts.camera_not_found");
stream.getTracks().forEach((track) => track.stop());
@@ -911,9 +911,9 @@ export default {
}
this.scannerStream = stream;
const video = this.$refs.scannerVideo;
- if (!video) return;
- video.srcObject = stream;
- await video.play();
+ if (!(await attachStreamToVideo(stream, video))) {
+ return;
+ }
this.detectQrLoop();
} catch (e) {
this.scannerError = this.describeCameraError(e);
@@ -922,25 +922,23 @@ export default {
detectQrLoop() {
if (!this.isScannerDialogOpen) return;
const video = this.$refs.scannerVideo;
- if (!video || video.readyState < 2) {
- this.scannerAnimationFrame = requestAnimationFrame(() => this.detectQrLoop());
- return;
- }
- const detector = new window.BarcodeDetector({ formats: ["qr_code"] });
- detector
- .detect(video)
- .then((barcodes) => {
- const qr = barcodes?.[0]?.rawValue;
+ decodeQrFromVideo(video)
+ .then((qr) => {
+ if (!this.isScannerDialogOpen) {
+ return;
+ }
if (qr) {
- this.newContactInput = qr.trim();
+ this.newContactInput = qr;
this.closeScannerDialog();
ToastUtils.success(this.$t("contacts.qr_scanned"));
- } else {
- this.scannerAnimationFrame = requestAnimationFrame(() => this.detectQrLoop());
+ return;
}
+ this.scannerAnimationFrame = requestAnimationFrame(() => this.detectQrLoop());
})
.catch(() => {
- this.scannerAnimationFrame = requestAnimationFrame(() => this.detectQrLoop());
+ if (this.isScannerDialogOpen) {
+ this.scannerAnimationFrame = requestAnimationFrame(() => this.detectQrLoop());
+ }
});
},
stopScanner() {
@@ -958,14 +956,11 @@ export default {
this.stopScanner();
},
describeCameraError(error) {
- const name = error?.name || "";
- if (name === "NotAllowedError" || name === "SecurityError") {
- return this.$t("contacts.camera_permission_denied");
- }
- if (name === "NotFoundError" || name === "DevicesNotFoundError") {
- return this.$t("contacts.camera_not_found");
- }
- return this.$t("contacts.camera_failed");
+ return describeQrCameraError(error, {
+ permissionDenied: this.$t("contacts.camera_permission_denied"),
+ notFound: this.$t("contacts.camera_not_found"),
+ failed: this.$t("contacts.camera_failed"),
+ });
},
},
};
diff --git a/meshchatx/src/frontend/components/messages/MessagesPage.vue b/meshchatx/src/frontend/components/messages/MessagesPage.vue
index 4bfb6c99..43841ca9 100644
--- a/meshchatx/src/frontend/components/messages/MessagesPage.vue
+++ b/meshchatx/src/frontend/components/messages/MessagesPage.vue
@@ -323,6 +323,13 @@ import DialogUtils from "../../js/DialogUtils";
import DownloadUtils from "../../js/DownloadUtils";
import GlobalEmitter from "../../js/GlobalEmitter";
import ToastUtils from "../../js/ToastUtils";
+import {
+ attachStreamToVideo,
+ decodeQrFromVideo,
+ describeCameraError as describeQrCameraError,
+ isCameraSupported,
+ startCameraStream,
+} from "../../js/qrScannerUtils";
import { lxmfConversationListPreview } from "../../js/lxmfConversationPreview";
import { loadMessagePanes, saveMessagePanes } from "../../js/browserLayoutStore";
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
@@ -472,11 +479,7 @@ export default {
return this.messagesSidebarPosition === "right";
},
cameraSupported() {
- return (
- typeof window !== "undefined" &&
- typeof window.BarcodeDetector !== "undefined" &&
- navigator?.mediaDevices?.getUserMedia
- );
+ return isCameraSupported();
},
},
watch: {
@@ -1521,19 +1524,14 @@ export default {
return;
}
try {
- const stream = await navigator.mediaDevices.getUserMedia({
- video: { facingMode: "environment" },
- audio: false,
- });
+ const stream = await startCameraStream();
this.ingestScannerStream = stream;
const video = this.$refs.ingestScannerVideo;
- if (!video) {
+ if (!(await attachStreamToVideo(stream, video))) {
this.ingestScannerError = this.$t("messages.camera_failed");
this.stopIngestScanner();
return;
}
- video.srcObject = stream;
- await video.play();
this.detectIngestQrLoop();
} catch (e) {
this.ingestScannerError = this.describeCameraError(e);
@@ -1542,15 +1540,11 @@ export default {
detectIngestQrLoop() {
if (!this.isIngestScannerModalOpen) return;
const video = this.$refs.ingestScannerVideo;
- if (!video || video.readyState < 2) {
- this.ingestScannerAnimationFrame = requestAnimationFrame(() => this.detectIngestQrLoop());
- return;
- }
- const detector = new window.BarcodeDetector({ formats: ["qr_code"] });
- detector
- .detect(video)
- .then((barcodes) => {
- const qr = barcodes?.[0]?.rawValue?.trim();
+ decodeQrFromVideo(video)
+ .then((qr) => {
+ if (!this.isIngestScannerModalOpen) {
+ return;
+ }
if (!qr) {
this.ingestScannerAnimationFrame = requestAnimationFrame(() => this.detectIngestQrLoop());
return;
@@ -1565,7 +1559,9 @@ export default {
this.ingestPaperMessage();
})
.catch(() => {
- this.ingestScannerAnimationFrame = requestAnimationFrame(() => this.detectIngestQrLoop());
+ if (this.isIngestScannerModalOpen) {
+ this.ingestScannerAnimationFrame = requestAnimationFrame(() => this.detectIngestQrLoop());
+ }
});
},
stopIngestScanner() {
@@ -1579,14 +1575,11 @@ export default {
}
},
describeCameraError(error) {
- const name = error?.name || "";
- if (name === "NotAllowedError" || name === "SecurityError") {
- return this.$t("messages.camera_permission_denied");
- }
- if (name === "NotFoundError" || name === "DevicesNotFoundError") {
- return this.$t("messages.camera_not_found");
- }
- return this.$t("messages.camera_failed");
+ return describeQrCameraError(error, {
+ permissionDenied: this.$t("messages.camera_permission_denied"),
+ notFound: this.$t("messages.camera_not_found"),
+ failed: this.$t("messages.camera_failed"),
+ });
},
async pasteFromClipboard() {
try {
diff --git a/meshchatx/src/frontend/components/tools/PaperMessagePage.vue b/meshchatx/src/frontend/components/tools/PaperMessagePage.vue
index 5fcc86f3..06af39bd 100644
--- a/meshchatx/src/frontend/components/tools/PaperMessagePage.vue
+++ b/meshchatx/src/frontend/components/tools/PaperMessagePage.vue
@@ -281,6 +281,13 @@ import QRCode from "qrcode";
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import WebSocketConnection from "../../js/WebSocketConnection";
import ToastUtils from "../../js/ToastUtils";
+import {
+ attachStreamToVideo,
+ decodeQrFromVideo,
+ describeCameraError as describeQrCameraError,
+ isCameraSupported,
+ startCameraStream,
+} from "../../js/qrScannerUtils";
import ToolsPageHeader from "./ToolsPageHeader.vue";
export default {
@@ -306,11 +313,7 @@ export default {
return this.destinationHash.length === 32 && this.content.length > 0;
},
cameraSupported() {
- return (
- typeof window !== "undefined" &&
- typeof window.BarcodeDetector !== "undefined" &&
- navigator?.mediaDevices?.getUserMedia
- );
+ return isCameraSupported();
},
},
mounted() {
@@ -412,19 +415,14 @@ export default {
return;
}
try {
- const stream = await navigator.mediaDevices.getUserMedia({
- video: { facingMode: "environment" },
- audio: false,
- });
+ const stream = await startCameraStream();
this.ingestScannerStream = stream;
const video = this.$refs.ingestScannerVideo;
- if (!video) {
+ if (!(await attachStreamToVideo(stream, video))) {
this.ingestScannerError = this.$t("messages.camera_failed");
this.stopIngestScanner();
return;
}
- video.srcObject = stream;
- await video.play();
this.detectIngestQrLoop();
} catch (e) {
this.ingestScannerError = this.describeCameraError(e);
@@ -433,15 +431,11 @@ export default {
detectIngestQrLoop() {
if (!this.isIngestScannerModalOpen) return;
const video = this.$refs.ingestScannerVideo;
- if (!video || video.readyState < 2) {
- this.ingestScannerAnimationFrame = requestAnimationFrame(() => this.detectIngestQrLoop());
- return;
- }
- const detector = new window.BarcodeDetector({ formats: ["qr_code"] });
- detector
- .detect(video)
- .then((barcodes) => {
- const qr = barcodes?.[0]?.rawValue?.trim();
+ decodeQrFromVideo(video)
+ .then((qr) => {
+ if (!this.isIngestScannerModalOpen) {
+ return;
+ }
if (!qr) {
this.ingestScannerAnimationFrame = requestAnimationFrame(() => this.detectIngestQrLoop());
return;
@@ -456,7 +450,9 @@ export default {
this.ingestPaperMessage();
})
.catch(() => {
- this.ingestScannerAnimationFrame = requestAnimationFrame(() => this.detectIngestQrLoop());
+ if (this.isIngestScannerModalOpen) {
+ this.ingestScannerAnimationFrame = requestAnimationFrame(() => this.detectIngestQrLoop());
+ }
});
},
stopIngestScanner() {
@@ -470,14 +466,11 @@ export default {
}
},
describeCameraError(error) {
- const name = error?.name || "";
- if (name === "NotAllowedError" || name === "SecurityError") {
- return this.$t("messages.camera_permission_denied");
- }
- if (name === "NotFoundError" || name === "DevicesNotFoundError") {
- return this.$t("messages.camera_not_found");
- }
- return this.$t("messages.camera_failed");
+ return describeQrCameraError(error, {
+ permissionDenied: this.$t("messages.camera_permission_denied"),
+ notFound: this.$t("messages.camera_not_found"),
+ failed: this.$t("messages.camera_failed"),
+ });
},
async copyUri() {
try {
diff --git a/meshchatx/src/frontend/js/qrScannerUtils.js b/meshchatx/src/frontend/js/qrScannerUtils.js
new file mode 100644
index 00000000..598bd8d3
--- /dev/null
+++ b/meshchatx/src/frontend/js/qrScannerUtils.js
@@ -0,0 +1,108 @@
+import jsQR from "jsqr";
+
+let decodeCanvas = null;
+
+export function resetDecodeCanvasForTests() {
+ decodeCanvas = null;
+}
+
+export function isCameraSupported() {
+ return typeof window !== "undefined" && !!navigator?.mediaDevices?.getUserMedia;
+}
+
+export function getScannerVideoConstraints() {
+ return {
+ video: {
+ facingMode: { ideal: "environment" },
+ width: { ideal: 1280 },
+ height: { ideal: 720 },
+ },
+ audio: false,
+ };
+}
+
+export function describeCameraError(error, messages) {
+ const name = error?.name || "";
+ if (name === "NotAllowedError" || name === "SecurityError") {
+ return messages.permissionDenied;
+ }
+ if (name === "NotFoundError" || name === "DevicesNotFoundError") {
+ return messages.notFound;
+ }
+ return messages.failed;
+}
+
+function getDecodeCanvas() {
+ if (!decodeCanvas) {
+ decodeCanvas = document.createElement("canvas");
+ }
+ return decodeCanvas;
+}
+
+async function decodeWithBarcodeDetector(video) {
+ if (typeof window.BarcodeDetector === "undefined") {
+ return null;
+ }
+ const detector = new window.BarcodeDetector({ formats: ["qr_code"] });
+ const barcodes = await detector.detect(video);
+ return barcodes?.[0]?.rawValue?.trim() || null;
+}
+
+function decodeWithJsQr(video) {
+ const width = video.videoWidth;
+ const height = video.videoHeight;
+ if (!width || !height) {
+ return null;
+ }
+ const canvas = getDecodeCanvas();
+ canvas.width = width;
+ canvas.height = height;
+ const context = canvas.getContext("2d", { willReadFrequently: true });
+ if (!context) {
+ return null;
+ }
+ context.drawImage(video, 0, 0, width, height);
+ const imageData = context.getImageData(0, 0, width, height);
+ const result = jsQR(imageData.data, width, height, { inversionAttempts: "attemptBoth" });
+ return result?.data?.trim() || null;
+}
+
+export async function decodeQrFromVideo(video) {
+ if (!video || video.readyState < 2) {
+ return null;
+ }
+ try {
+ const nativeValue = await decodeWithBarcodeDetector(video);
+ if (nativeValue) {
+ return nativeValue;
+ }
+ } catch {
+ // Fall back to canvas decoding below.
+ }
+ return decodeWithJsQr(video);
+}
+
+export async function startCameraStream() {
+ const stream = await navigator.mediaDevices.getUserMedia(getScannerVideoConstraints());
+ const track = stream.getVideoTracks()[0];
+ if (track?.getCapabilities) {
+ const capabilities = track.getCapabilities();
+ if (capabilities.focusMode?.includes("continuous")) {
+ try {
+ await track.applyConstraints({ advanced: [{ focusMode: "continuous" }] });
+ } catch {
+ // Optional; some WebViews reject advanced focus constraints.
+ }
+ }
+ }
+ return stream;
+}
+
+export async function attachStreamToVideo(stream, video) {
+ if (!video) {
+ return false;
+ }
+ video.srcObject = stream;
+ await video.play();
+ return true;
+}
diff --git a/package.json b/package.json
index 52e285c5..32a5694c 100644
--- a/package.json
+++ b/package.json
@@ -288,6 +288,7 @@
"electron-prompt": "^1.7.0",
"emoji-picker-element": "^1.29.1",
"emoji-picker-element-data": "^1.8.0",
+ "jsqr": "^1.4.0",
"jszip": "^3.10.1",
"marked": "^18.0.5",
"mitt": "^3.0.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bb7aee39..baa379a1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -92,6 +92,9 @@ importers:
emoji-picker-element-data:
specifier: ^1.8.0
version: 1.8.0
+ jsqr:
+ specifier: ^1.4.0
+ version: 1.4.0
jszip:
specifier: ^3.10.1
version: 3.10.1
@@ -2254,6 +2257,9 @@ packages:
jsonfile@6.0.1:
resolution: {integrity: sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg==}
+ jsqr@1.4.0:
+ resolution: {integrity: sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==}
+
jszip@3.10.1:
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
@@ -3901,7 +3907,7 @@ snapshots:
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
- string-width-cjs: string-width@4.2.3
+ string-width-cjs: string-width@4.2.0
strip-ansi: 6.0.1
strip-ansi-cjs: strip-ansi@6.0.1
wrap-ansi: 8.1.0
@@ -5866,6 +5872,8 @@ snapshots:
optionalDependencies:
graceful-fs: 4.2.0
+ jsqr@1.4.0: {}
+
jszip@3.10.1:
dependencies:
lie: 3.3.0
diff --git a/tests/frontend/apiFetchGuard.test.js b/tests/frontend/apiFetchGuard.test.js
index 4b01d670..b8bd7036 100644
--- a/tests/frontend/apiFetchGuard.test.js
+++ b/tests/frontend/apiFetchGuard.test.js
@@ -37,7 +37,9 @@ describe("API client usage guard", () => {
if (!src.includes("/api/v1") || !src.includes("fetch(")) {
continue;
}
- if (/fetch\s*\([\s\S]{0,400}?\/api\/v1[\s\S]{0,400}?method\s*:\s*["'](POST|PUT|PATCH|DELETE)["']/i.test(src)) {
+ if (
+ /fetch\s*\([\s\S]{0,400}?\/api\/v1[\s\S]{0,400}?method\s*:\s*["'](POST|PUT|PATCH|DELETE)["']/i.test(src)
+ ) {
offenders.push(relativePath(file));
}
}
diff --git a/tests/frontend/qrScannerUtils.test.js b/tests/frontend/qrScannerUtils.test.js
new file mode 100644
index 00000000..8006f265
--- /dev/null
+++ b/tests/frontend/qrScannerUtils.test.js
@@ -0,0 +1,89 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+vi.mock("jsqr", () => ({
+ default: vi.fn(),
+}));
+
+import jsQR from "jsqr";
+import {
+ decodeQrFromVideo,
+ describeCameraError,
+ getScannerVideoConstraints,
+ isCameraSupported,
+ resetDecodeCanvasForTests,
+} from "@/js/qrScannerUtils";
+
+describe("qrScannerUtils", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ resetDecodeCanvasForTests();
+ Object.defineProperty(global.navigator, "mediaDevices", {
+ configurable: true,
+ value: {
+ getUserMedia: vi.fn(),
+ },
+ });
+ });
+
+ afterEach(() => {
+ delete global.window.BarcodeDetector;
+ });
+
+ it("reports camera support when getUserMedia exists", () => {
+ expect(isCameraSupported()).toBe(true);
+ });
+
+ it("prefers environment camera with higher resolution", () => {
+ const constraints = getScannerVideoConstraints();
+ expect(constraints.video.facingMode).toEqual({ ideal: "environment" });
+ expect(constraints.video.width).toEqual({ ideal: 1280 });
+ expect(constraints.video.height).toEqual({ ideal: 720 });
+ });
+
+ it("maps permission errors to the permission message", () => {
+ expect(
+ describeCameraError(
+ { name: "NotAllowedError" },
+ {
+ permissionDenied: "denied",
+ notFound: "missing",
+ failed: "failed",
+ }
+ )
+ ).toBe("denied");
+ });
+
+ it("uses BarcodeDetector when available", async () => {
+ class MockBarcodeDetector {
+ detect() {
+ return Promise.resolve([{ rawValue: " lxma://abc " }]);
+ }
+ }
+ global.window.BarcodeDetector = MockBarcodeDetector;
+ const video = { readyState: 2, videoWidth: 640, videoHeight: 480 };
+ await expect(decodeQrFromVideo(video)).resolves.toBe("lxma://abc");
+ expect(jsQR).not.toHaveBeenCalled();
+ });
+
+ it("falls back to jsQR when BarcodeDetector is unavailable", async () => {
+ jsQR.mockReturnValue({ data: "lxmf://deadbeef" });
+ const getContext = vi.fn(() => ({
+ drawImage: vi.fn(),
+ getImageData: vi.fn(() => ({ data: new Uint8ClampedArray(4) })),
+ }));
+ const canvas = {
+ width: 0,
+ height: 0,
+ getContext,
+ };
+ vi.spyOn(document, "createElement").mockReturnValue(canvas);
+ const video = {
+ readyState: 2,
+ videoWidth: 2,
+ videoHeight: 2,
+ };
+
+ await expect(decodeQrFromVideo(video)).resolves.toBe("lxmf://deadbeef");
+ expect(jsQR).toHaveBeenCalled();
+ });
+});
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────